<?php
/**
 * MYSQL类
 * 
 * Copyright (c) 2014~2019 https://desion.net All rights reserved.
 * Author: 阿林 QQ:120867629 <linhong00@gmail.com>
 */
declare (strict_types = 1);
namespace dsphp\db\common;
use PDO;
use PDOStatement;

abstract class PDOConnection extends Connection
{
    const PARAM_FLOAT = 21;

    // PDO操作实例
    protected $PDOStatement;
    // 查询结果类型
    protected $fetchType = PDO::FETCH_ASSOC;
    // 字段属性大小写
    // protected $attrCase = PDO::CASE_LOWER;
    // PDO连接参数
    protected $params = [
        PDO::ATTR_CASE              => PDO::CASE_NATURAL,
        PDO::ATTR_ERRMODE           => PDO::ERRMODE_EXCEPTION,
        PDO::ATTR_ORACLE_NULLS      => PDO::NULL_NATURAL,
        PDO::ATTR_STRINGIFY_FETCHES => false,
        PDO::ATTR_EMULATE_PREPARES  => false,
    ];

    /**
     * 解析pdo连接的dsn信息
     *
     * @param  array $config 连接信息
     * @return string
     */
    abstract protected function parseDsn(array $config);

    /**
     * 初始化数据库连接
     *
     * @param boolean $master 是否主服务器
     * @return void
     */
    protected function initConnect(bool $master = true): void
    {
        if ($this->config['deploy']) {
            // 采用分布式数据库
            if ($master || $this->transTimes) {
                if (! $this->linkWrite) {
                    $this->linkWrite = $this->multiConnect(true);
                }

                $this->linkID = $this->linkWrite;
            } else {
                if (! $this->linkRead) {
                    $this->linkRead = $this->multiConnect(false);
                }

                $this->linkID = $this->linkRead;
            }
        } elseif (! $this->linkID) {
            // 默认单数据库
            $this->linkID = $this->connect();
        }
    }

    /**
     * 连接分布式服务器
     *
     * @param boolean $master 主服务器
     * @return PDO
     */
    protected function multiConnect(bool $master = false): PDO
    {
        $config  = [];
        $keyList = ['username', 'password', 'hostname', 'hostport', 'database', 'dsn', 'charset'];
        // 分布式数据库配置解析
        foreach ($keyList as $name) {
            $config[$name] = is_string($this->config[$name]) ? explode(',', $this->config[$name]) : $this->config[$name];
        }

        // 主服务器序号
        if ($this->config['master_num'] > 1) {
            $m = floor(mt_rand(0, $this->config['master_num'] - 1));
        } else {
            $m = 0;
        }

        if ($this->config['rw_separate']) {
            // 主从式采用读写分离
            if ($master) // 主服务器写入
            {
                $r = $m;
            } elseif (is_numeric($this->config['slave_no'])) {
                // 指定服务器读
                $r = $this->config['slave_no'];
            } else {
                // 读操作连接从服务器 每次随机连接的数据库
                $r = floor(mt_rand($this->config['master_num'], count($config['hostname']) - 1));
            }
        } else {
            // 读写操作不区分服务器 每次随机连接的数据库
            $r = floor(mt_rand(0, count($config['hostname']) - 1));
        }
        $dbMaster = false;

        if ($m != $r) {
            $dbMaster = [];
            foreach ($keyList as $name) {
                $dbMaster[$name] = $config[$name][$m] ?? $config[$name][0];
            }
        }

        $dbConfig = [];

        foreach ($keyList as $name) {
            $dbConfig[$name] = $config[$name][$r] ?? $config[$name][0];
        }

        return $this->connect($dbConfig, $r, $r == $m ? false : $dbMaster);
    }

    /**
     * 连接数据库方法
     *
     * @param array      $config         连接参数
     * @param integer    $linkNum        连接序号
     * @param array|bool $autoConnection 是否自动连接主数据库（用于分布式）
     * @return PDO
     * @throws PDOException
     */
    public function connect(array $config = [], $linkNum = 0, $autoConnection = false): PDO
    {
        if (isset($this->links[$linkNum])) {
            return $this->links[$linkNum];
        }

        if (empty($config)) {
            $config = $this->config;
        } else {
            $config = array_merge($this->config, $config);
        }

        // 连接参数
        if (isset($config['params']) && is_array($config['params'])) {
            $params = $config['params'] + $this->params;
        } else {
            $params = $this->params;
        }

        // if (! empty($config['break_match_str'])) {
        //     $this->breakMatchStr = array_merge($this->breakMatchStr, (array) $config['break_match_str']);
        // }

        try {
            if (empty($config['dsn'])) {
                $config['dsn'] = $this->parseDsn($config);
            }

            $startTime             = microtime(true);
            $this->links[$linkNum] = $this->createPdo($config['dsn'], $config['username'], $config['password'], $params);
            // 记录数据库连接信息
            $this->db->log('CONNECT:[ UseTime:' . number_format(microtime(true) - $startTime, 6) . 's ] ' . $config['dsn']);

            return $this->links[$linkNum];
        } catch (\PDOException $e) {
            if ($autoConnection) {
                $this->db->log($e->getMessage(), 'error');
                return $this->connect($autoConnection, $linkNum);
            } else {
                throw $e;
            }
        }
    }

    /**
     * 创建PDO实例
     * @param $dsn
     * @param $username
     * @param $password
     * @param $params
     * @return PDO
     */
    protected function createPdo($dsn, $username, $password, $params)
    {
        return new PDO($dsn, $username, $password, $params);
    }

    /**
     * 释放查询结果
     */
    public function free(): void
    {
        $this->PDOStatement = null;
    }

    /**
     * 执行查询 返回数据集
     *
     * @param mixed     $sql   sql指令
     * @return array
     */
    public function query($sql): array
    {
        $master    = $this->options['master'] ?? false;
        $procedure = $this->_checkIsProcedure($sql);

        $this->getPDOStatement($sql, $master, $procedure);

        $resultSet = $this->getResult($procedure);

        return $resultSet;
    }

    /**
     * 执行语句
     *
     * @param string    $sql    sql指令
     * @param bool      $origin 是否原生查询
     * @return int
     */
    public function execute(string $sql, bool $origin = false): int
    {
        $master    = true;
        $procedure = $this->_checkIsProcedure($sql);

        // if (!$origin && !empty($this->config['deploy']) && !empty($this->config['read_master'])) {
        //     $this->readMaster = true;
        // }

        $this->getPDOStatement($sql, $master, $procedure);

        $this->numRows = $this->PDOStatement->rowCount();

        return $this->numRows;
    }
    // 是否存储过程
    protected function _checkIsProcedure(string $sql): bool
    {
        $procedure = $this->options['procedure'] ?? false;
        $procedure = $procedure ? true : in_array(strtolower(substr(trim($sql), 0, 4)), ['call', 'exec']);

        return $procedure;
    }

    /**
     * 执行查询但只返回PDOStatement对象
     *
     * @param string $sql       sql指令
     * @param bool   $master    是否在主服务器读操作
     * @param bool   $procedure 是否为存储过程调用
     * @return PDOStatement
     * @throws BindParamException
     * @throws \PDOException
     * @throws \Exception
     * @throws \Throwable
     */
    public function getPDOStatement(string $sql, bool $master = false, bool $procedure = false): PDOStatement
    {
        $this->initConnect($master);

        // 记录SQL语句
        $this->queryStr = $sql;

        $this->db->updateQueryTimes();

        $this->db->log($sql);

        try {
            $this->queryStartTime = microtime(true);

            // 预处理
            $this->PDOStatement = $this->linkID->prepare($sql);
            // 执行查询
            $this->PDOStatement->execute();

            return $this->PDOStatement;
        } catch (\Throwable | \Exception $e) {
            // if ($this->isBreak($e)) {
            //     return $this->close()->getPDOStatement($sql, $bind, $master, $procedure);
            // }

            if ($e instanceof \PDOException) {
                throw new PDOException($e, $this->config, $this->getLastsql());
            } else {
                throw $e;
            }
        }
    }

    /**
     * 获得数据集数组
     *
     * @param bool $procedure 是否存储过程
     */
    protected function getResult(bool $procedure = false): array
    {
        if ($procedure) {
            // 存储过程返回结果
            return $this->procedure();
        }

        $result = $this->PDOStatement->fetchAll($this->fetchType);

        $this->numRows = count($result);

        return $result;
    }

    /**
     * 获取最近插入的ID
     *
     * @param string    $sequence 自增序列名
     * @return mixed
     */
    public function getLastInsID(string $sequence = null)
    {
        try {
            $insertId = $this->linkID->lastInsertId($sequence);
        } catch (\Exception $e) {
            $insertId = '';
        }

        return $insertId;
    }

    /**
     * 获取最近一次查询的sql语句
     */
    public function getLastSql(): string
    {
        return $this->queryStr;
    }
}
